package fr.labri.buganalyzer.extractors;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import fr.labri.buganalyzer.model.Attachement;
import fr.labri.buganalyzer.model.Change;
import fr.labri.buganalyzer.model.Comment;
import fr.labri.buganalyzer.model.Developer;
import fr.labri.buganalyzer.model.Issue;
import fr.labri.buganalyzer.model.Tracker;

public class BugzillaExtractor extends AbstractExtractor {

	private XMLInputFactory inFactory;

	public BugzillaExtractor(String url) {
		super();
		this.tracker = new Tracker(BugzillaUtils.trackerType, url);
		this.issues = new HashSet<Issue>();
		this.developers = new HashSet<Developer>();
		this.comments = new HashSet<Comment>();
		this.attachements = new HashSet<Attachement>();
		this.changes = new HashSet<Change>();

		// Setting up the factory properties
		inFactory = XMLInputFactory.newInstance();
		inFactory.setProperty(XMLInputFactory.IS_COALESCING, false);
		inFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
		inFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
	}

	public List<String> extractComponentsFromHTMLUrl(String componentsListUrl) {
		Document doc;
		List<String> componentsList = new ArrayList<String>();
		try {
			doc = Jsoup.connect(componentsListUrl).timeout(timeout).get();
			Elements content = doc.getElementsByClass("component_name");
			for (Element com : content) {
				String compName = "";
				Elements elem = com.getElementsByTag("a");
				for (Element e : elem) {
					compName = e.text();
					break;
				}
				componentsList.add(compName);
			}
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		return componentsList;
	}

	public void extractAllBugsHistories() {

		System.out.print("Getting changes ..");
		for (Issue i : this.issues) {
			try {
				List<Change> history = extractBugHistory(i.getNumber());
				Thread.sleep(1000);
				System.out.print(".");
				for (Change c : history) {
					c.setIssue(i);
					this.changes.add(c);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println();

	}

	public List<Change> extractBugHistory(int bugNumber) {
		List<Change> history = new ArrayList<Change>();
		String bugHistoryUrl = tracker.getUrl() + BugzillaUtils.bgzShowActivityToken + bugNumber;
		Document doc;
		try {
			doc = Jsoup.connect(bugHistoryUrl).timeout(timeout).get();
			Element element = doc.getElementById("bugzilla-body");

			for (Element table : element.select("table")) {

				Developer dev = null;
				String when = "";
				for (Element row : table.select("tr")) {
					Elements tds = row.select("td");
					Change c = new Change();
					if (tds.size() == 5) {//
						dev = getDevelperByName(tds.get(0).text());
						c.setChangedBy(dev);
						when = tds.get(1).text();
						c.setChangedOn(BugzillaUtils.getTimestamp(when));
						String what = tds.get(2).text();
						String old = tds.get(3).text();
						String _new = tds.get(4).text();
						c.setField(what);
						c.setOldValue(old);
						c.setNewValue(_new);
						history.add(c);
					}
					if (tds.size() == 3) {
						Change cc = new Change();
						cc.setChangedBy(dev);
						cc.setChangedOn(BugzillaUtils.getTimestamp(when));
						String what = tds.get(0).text();
						String old = tds.get(1).text();
						String _new = tds.get(2).text();

						cc.setField(what);
						cc.setOldValue(old);
						cc.setNewValue(_new);
						history.add(cc);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return history;
	}

	protected Developer getDevelperByName(String alias) {
		for (Developer dev : developers) {
			if (dev.getAlias().equals(alias))
				return dev;
		}
		Developer d = new Developer(alias);
		d.setAlias(alias);
		return d;
	}

	public List<Integer> extractBuglist(String bugListUrl) throws IOException {
		List<Integer> bugs = new ArrayList<Integer>();
		Document doc = Jsoup.connect(bugListUrl).timeout(timeout).get();
		Elements content = doc.getElementsByClass("bz_bugitem");
		for (Element elem : content) {
			int bugId = 0;
			Elements bugInfo = elem.getElementsByTag("a");
			for (Element e : bugInfo) {
				bugId = Integer.parseInt(e.text());
				break;
			}
			bugs.add(bugId);
		}
		return bugs;
	}

	public Issue extractBugDetailsFromXMLUrl(String urlString) {
		XMLEventReader eventReader;
		Issue result = null;
		try {
			URL url = new URL(urlString);
			URLConnection conn = url.openConnection();
			eventReader = inFactory.createXMLEventReader(conn.getInputStream());
			while (eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();
				if (event.isStartElement() && ((StartElement) event).getName().toString().equals("bug"))
					result = parseBugEvent(eventReader);
			}
		} catch (XMLStreamException e1) {
			e1.printStackTrace();
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}

	public Issue extractBugDetailsFromFile(String xmlFile) throws FileNotFoundException {
		Issue result = null;
		FileReader fileReader = new FileReader(xmlFile);
		BufferedReader buffReader = new BufferedReader(fileReader);
		XMLEventReader eventReader = null;
		try {
			eventReader = inFactory.createXMLEventReader(buffReader);
			while (eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();
				if (event.isStartElement() && ((StartElement) event).getName().toString().equals("bug"))
					result = parseBugEvent(eventReader);
			}
		} catch (XMLStreamException e1) {
			e1.printStackTrace();
		}
		return result;
	}

	public void extractAllBugsFromXmlFile(String fileUri) {
		FileReader fileReader;
		XMLEventReader eventReader = null;
		XMLEvent event;
		try {
			fileReader = new FileReader(fileUri);
			BufferedReader buffReader = new BufferedReader(fileReader);
			eventReader = inFactory.createXMLEventReader(buffReader);
			while (eventReader.hasNext()) {
				event = eventReader.nextEvent();
				Issue currentBug;
				if (event.isStartElement() && ((StartElement) event).getName().toString().equals("bug")) {
					currentBug = parseBugEvent(eventReader);
					issues.add(currentBug);
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (XMLStreamException e1) {
			e1.printStackTrace();
		}
	}

	private String getEventData(XMLEventReader eventReader) {
		XMLEvent e;
		try {
			e = eventReader.nextEvent();
			if (e.isCharacters())
				return ((Characters) e).getData().replaceAll("\n", "").trim();
			else
				return null;
		} catch (XMLStreamException e1) {
			e1.printStackTrace();
			return null;
		}

	}

	private Issue parseBugEvent(XMLEventReader eventReader) {
		Issue result = new Issue();
		try {
			XMLEvent event;
			while (eventReader.hasNext()) {
				event = eventReader.nextEvent();
				if (event.isStartElement()) {
					String s = ((StartElement) event).getName().toString();
					if (s.equals("bug_id"))
						result.setNumber(Integer.parseInt(getEventData(eventReader)));
					if (s.equals("creation_ts"))
						result.setSubmittedOn(BugzillaUtils.getTimestamp(getEventData(eventReader)));
					if (s.equals("short_desc"))
						result.setSummary(getEventData(eventReader));
					if (s.equals("product"))
						result.setProduct(getEventData(eventReader));
					if (s.equals("component"))
						result.setComponent(getEventData(eventReader));
					if (s.equals("version"))
						result.setVersion(getEventData(eventReader));
					if (s.equals("priority"))
						result.setPriority(getEventData(eventReader));
					if (s.equals("resolution"))
						result.setResolution(getEventData(eventReader));
					if (s.equals("bug_status"))
						result.setStatus(getEventData(eventReader));
					if (s.equals("bug_severity"))
						result.setSeverity(getEventData(eventReader));
					if (s.equals("reporter")) {
						result.setReporter(getDeveloper(eventReader, event));
					}
					if (s.equals("assigned_to")) {
						result.setAssignee(getDeveloper(eventReader, event));
					}
					if (s.equals("cc")) {
						result.getCc().add(getEventData(eventReader));
					}
					if (s.equals("long_desc")) {
						Comment cmt = parseComment(eventReader);
						cmt.setIssue(result);
						result.getComments().add(cmt);
					}

				} else if (event.isEndElement()) {
					if (((EndElement) event).getName().toString().equals("bug"))
						return result;
				}
			}
		} catch (XMLStreamException e2) {
			e2.printStackTrace();
		}
		return result;
	}

	private Developer getDeveloper(XMLEventReader eventReader, XMLEvent event) {
		Attribute att = ((StartElement) event).getAttributeByName(new QName("name"));
		String devName = "";
		if (att != null) {
			devName = att.getValue();
			for (Developer dev : developers)
				if (dev.getName().equals(devName))
					return dev;

			Developer d = new Developer(devName);
			d.setAlias(getEventData(eventReader));
			developers.add(d);
			return d;
		} else
			return null;
	}

	private Comment parseComment(XMLEventReader eventReader) {
		Comment cmt = new Comment();
		try {
			XMLEvent event;
			while (eventReader.hasNext()) {
				event = eventReader.nextEvent();
				if (event.isStartElement()) {
					String s = ((StartElement) event).getName().toString();
					if (s.equals("commentid"))
						cmt.setNativeId(Integer.parseInt(getEventData(eventReader)));
					if (s.equals("who")) {
						cmt.setSubmitter(getDeveloper(eventReader, event));
					}
					if (s.equals("bug_when"))
						cmt.setSubmittedOn(BugzillaUtils.getTimestamp(getEventData(eventReader)));
					if (s.equals("thetext")) {
						String ss = getEventData(eventReader);
						if (ss != null && ss.length() > 255)
							ss = ss.substring(0, 255);
						cmt.setText(ss);
						// TODO resove the hibernate type issue
					}

				} else if (event.isEndElement()) {
					if (((EndElement) event).getName().toString().equals("long_desc"))
						break;
				}
			}
		} catch (XMLStreamException e2) {
			e2.printStackTrace();
		}
		comments.add(cmt);
		return cmt;
	}

	public String buildQuery(List<Integer> bugList) {
		String result = BugzillaUtils.bgzBugXmlToken;
		for (Integer i : bugList) {
			result += "&id=" + i;
		}
		return result;
	}

	public void downloadXML(String query, String fileName) {
		StringBuilder xmlContent = new StringBuilder();
		Document document;
		try {
			document = Jsoup.connect(query).timeout(timeout).get();
			xmlContent.append(document.body().html());
			// save XML
			FileWriter fileWriter = new FileWriter(fileName);
			BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
			bufferedWriter.write(xmlContent.toString());
			bufferedWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/*
	 * Get the uri of the product as parameter then: - parse the product web
	 * page to collect the components list - for each component: - find the list
	 * of all related issues - construct the query to get the xml file of all
	 * found issues - store the xml file in the data current folder with the
	 * name of the component + timestamp
	 */
	public static void main(String[] args) {

		int wait = 1;
		String[] products = { "Mylyn Reviews R4E" };
		int total = 0;
		for (int k = 0; k < products.length; k++) {
			String product = products[k];
			String produrl = product.replace(" ", "%20");
			BugzillaExtractor be = new BugzillaExtractor(BugzillaUtils.eclipseBgzUrl);
			be.setTimeout(0); // infinit timeout
			try {
				// [Ant, Compare, CVS, Debug, Doc, IDE, Incubator, PMC, Releng,
				// Resources, Runtime, Scripting, Search, SWT, Team, Text, UI,
				// Update (deprecated - use RT>Equinox>p2), User Assistance,
				// WebDAV, Website]
				List<String> compList = be.extractComponentsFromHTMLUrl(BugzillaUtils.eclipseBgzUrl + BugzillaUtils.bgzProductToken + produrl);
				// [Languages, Resources, Runtime, Search, SWT, TM, UI, XWT]
				// for (String comp : compList) {
				for (int m = 0; m < compList.size(); m++) {
					String comp = compList.get(m);
					String compurl = comp.replace(" ", "%20");

					List<Integer> bugList = be.extractBuglist(BugzillaUtils.eclipseBgzUrl + BugzillaUtils.bgzBugListToken + produrl + "&component="
							+ compurl);
					// construct the query with all the bug ids and get the
					// xml
					// file

					int nbmax = 100;
					if (bugList.size() < nbmax) {
						String fileName = "./ressources/data/" + product + "_" + comp + ".xml";
						String query = BugzillaUtils.eclipseBgzUrl + be.buildQuery(bugList);
						be.downloadXML(query, fileName);
						System.out.println("Download " + fileName + " completed successfully..! " + bugList.size() + " bugs");
						total += bugList.size();
						System.out.println("Sleeping for " + wait + "s");
						Thread.sleep(wait * 1000);
					} else {
						// too many bugs, need to send several requests
						for (int i = 0; i < bugList.size(); i += nbmax) {
							List<Integer> tmpList = new ArrayList<Integer>();
							for (int j = i; j < i + nbmax && j < bugList.size(); j++) {
								tmpList.add(bugList.get(j));
							}
							String fileName = "./ressources/data/" + product + "_" + comp + "_" + i + ".xml";
							String query = BugzillaUtils.eclipseBgzUrl + be.buildQuery(tmpList);
							be.downloadXML(query, fileName);
							System.out.println("Download " + fileName + " completed successfully..! " + tmpList.size() + " bugs");
							total += tmpList.size();
							System.out.println("Sleeping for " + wait + "s");
							Thread.sleep(wait * 1000);
						}
					}
				}

				System.out.println("Job done for product: " + product + ". " + total + " bugs extracted until now");

			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public Issue getIssueByNumber(int num) {
		for (Issue i : this.getIssues()) {
			if (i.getNumber() == num)
				return i;
		}
		return null;
	}

	@Deprecated
	public List<Integer> extractBuglistFromFile(String buglistFile) throws IOException {
		List<Integer> bugs = new ArrayList<Integer>();
		File input = new File(buglistFile);
		Document doc = Jsoup.parse(input, "UTF-8", "");
		Elements content = doc.getElementsByClass("bz_bugitem");
		for (Element elem : content) {
			int bugId = 0;
			Elements bugInfo = elem.getElementsByTag("a");
			for (Element e : bugInfo) {
				bugId = Integer.parseInt(e.text());
				break;
			}
			bugs.add(bugId);
		}
		return bugs;
	}

	/**
	 * Their is no need to extract Issue details from the HTML url, use the
	 * extraction from the XML instead
	 * 
	 * @param bugFile
	 * @return Issue
	 * @throws IOException
	 */
	@Deprecated
	public Issue extractBugDetailsFromHTMLFile(String bugFile) throws IOException {
		File input = new File(bugFile);
		Document doc = Jsoup.parse(input, "UTF-8", "");
		// extracting the issue number and description
		Elements title = doc.getElementsByTag("title");
		Element tit = title.get(0);
		String[] s = tit.text().split("\\s");
		int bugId = Integer.parseInt(s[1]);
		String bugDesc = "";
		for (int i = 3; i < s.length; i++)
			bugDesc += new String(s[i] + " ");
		Issue bug = new Issue(bugId);
		bug.setSummary(bugDesc);
		String bugStatus = doc.getElementById("static_bug_status").text();
		bug.setStatus(bugStatus);

		return bug;
	}

	@Deprecated
	public List<String> extractComponentsFromFile(String fileLocation) throws IOException {
		File input = new File(fileLocation);
		Document doc = Jsoup.parse(input, "UTF-8", "");
		Elements content = doc.getElementsByClass("component_name");
		// Map to store each component with the uri of related issues
		List<String> componentsList = new ArrayList<String>();
		for (Element com : content) {
			String compName = com.text();
			componentsList.add(compName);
		}
		return componentsList;
	}

}
